/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using DaveSexton.DocProject.Engine;
using System.Reflection;
using System.IO;
using Microsoft.Ddue.Tools;
using System.Xml.XPath;
using System.Threading;
using DaveSexton.DocProject.Sandcastle.Resources;

namespace DaveSexton.DocProject.Sandcastle
{
	[Flags, System.ComponentModel.TypeConverter(typeof(EnumFlagsConverter)), BooleanValues("Enabled", "Disabled")]
	public enum BuildAssemblerOptions
	{
		[FlagDisplayName("None"), System.ComponentModel.Description("The build trace will not contain any output from Build Assembler and it cannot be canceled by a UI command, substantially improving performance.")]
		None = 0,

		[FlagDisplayName("Trace information"), System.ComponentModel.Description("Informational messages from Build Assembler will be included in the build trace.")]
		TraceInformation = 1,
		[FlagDisplayName("Trace warnings"), System.ComponentModel.Description("Build Assembler warnings will be included in the build trace and the Error List.")]
		TraceWarnings = 2,
		[FlagDisplayName("Trace errors"), System.ComponentModel.Description("Build Assembler errors will be included in the build trace and the Error List.")]
		TraceErrors = 4,

		[FlagDisplayName("Trace all"), System.ComponentModel.Description("All messages from Build Assembler will be included in the build trace; warnings and errors will also be appended to the Error List.")]
		TraceAll = TraceInformation | TraceWarnings | TraceErrors,

		[System.ComponentModel.Description("Build Assembler will be executed asynchronously so that the UI will remain responsive and so that it may be canceled by a UI command, at the cost of performance.")]
		Cancelable = 8
	}

	public class BuildAssemblerStep : BuildStep<SandcastleBuildEngine>
	{
		#region Public Properties
		public static bool BuildAssemblerApiLoaded
		{
			[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
			get
			{
				return BuildAssemblerLoader.Instance.TryLoadAssembly();
			}
		}

		public override string Name
		{
			get
			{
				return "Build Assembler {" + Path.GetFileName(configFile) + "}";
			}
		}

		public int TopicCount
		{
			get
			{
				return topicCount;
			}
		}
		#endregion

		#region Private / Protected
		private readonly BuildAssemblerOptions options;
		private readonly string configFile, manifestFile;
		private volatile int topicCount;
		private volatile BuildAssemblerController controller;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="BuildAssemblerStep" /> class.
		/// </summary>
		public BuildAssemblerStep(SandcastleBuildEngine engine, string manifestFile, string configFile, BuildAssemblerOptions options)
			: base(engine)
		{
			if (configFile == null)
				throw new ArgumentNullException("configFile");

			if (manifestFile == null)
				throw new ArgumentNullException("manifestFile");

			if ((options & BuildAssemblerOptions.Cancelable) == BuildAssemblerOptions.Cancelable)
				RunInBackground = true;

			this.configFile = configFile;
			this.manifestFile = manifestFile;
			this.options = options;
		}
		#endregion

		#region Methods
		protected override void CancelInternal(BuildContext context)
		{
			try
			{
				if (controller != null)
					controller.Abort();
			}
			// The AppDomainUnloadedException seemed to occur in testing when the AppDomain is unloaded before Abort returns to the caller.
			// It must happen when the Abort method cancels the worker thread and then returns control back to the Execute 
			// method, which then unloads the AppDomain before control-of-flow is returned to the async thread running this method.
			catch (AppDomainUnloadedException) { }
			catch (NullReferenceException) { }
		}

		protected override void ExecuteInternal(BuildContext context)
		{
			bool assemblerError = false;
			Exception assemblerEx = null;

			string previousDirectory = Environment.CurrentDirectory;
			Environment.CurrentDirectory = Engine.Settings.BuildAssemblerWorkingDirectory;

			try
			{
				context.TraceLine();
				context.TraceLine(Resources.Text.Preparing);

				// BuildAssembler seems to leak large amounts of memory and does not reset its shared
				// state after it executes.  Therefore, it is executed in a secondary AppDomain so it
				// can be unloaded after execution.  Unloading the AppDomain releases all of its memory.
				// Note that the AppDomain is not required to load the BuildAssembler assembly even though
				// it's delay-loaded (the purpose is not to avoid loading conflicts).  In testing, execution
				// inside VS's default AppDomain worked great, except for BuildAssembler's memory cleanup issues.
        AppDomainSetup setup = new AppDomainSetup();
        setup.ApplicationBase = SandcastleBuildEngineProvider.Paths.Tools;
        setup.ApplicationName = "DocProject: " + context.ProjectName;

        AppDomain domain = AppDomain.CreateDomain("Sandcastle Build Assembler: " + context.ProjectName, null, setup);
        domain.UnhandledException += domain_UnhandledException;

				try
				{
					Type type = typeof(BuildAssemblerController);

					controller = (BuildAssemblerController) domain.CreateInstanceAndUnwrap(
						type.Assembly.FullName, type.FullName, false, BindingFlags.Default, Type.DefaultBinder,
						new object[] { context, configFile, manifestFile, options },
						null, null, null);

					context.TraceLine(Resources.Text.Executing);

					if (!RunInBackground)
						System.Windows.Forms.Application.DoEvents();

					if (!context.Canceled)		// NOTE: must check context.Canceled after the controller has been created and before Execute is invoked so that Abort cannot be missed
						controller.Execute(SandcastleBuildEngineProvider.Paths.Tools);

					topicCount = controller.TopicCount;
					assemblerError = controller.AssemblerError;
					assemblerEx = controller.Exception;

					context.TraceLine();
					context.TraceLine(Text.TopicsProcessedCount, topicCount);
				}
				finally
				{
					try
					{
						AppDomain.Unload(domain);
					}
					catch (AppDomainUnloadedException) { }
					finally
					{
						GC.Collect();
					}
				}
			}
			finally
			{
				Environment.CurrentDirectory = previousDirectory;
			}

			if (assemblerEx != null)
				throw assemblerEx;
			else if (assemblerError)
				throw new InvalidOperationException(Errors.SandcastleBuildAssemblerFailed);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes")]
		private static void domain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
		{
			Exception ex = e.ExceptionObject as Exception;

			Log.Exception(ex ?? new Exception((e.ExceptionObject == null) ? Errors.UnknownError : e.ExceptionObject.ToString()),
				Errors.BuildAssemblerAppDomainUnhandledException, e.IsTerminating);
		}
		#endregion

		#region Nested
		private sealed class BuildAssemblerController : MarshalByRefObject
		{
			#region Public Properties
			public bool AssemblerError
			{
				get
				{
					return assemblerError;
				}
			}

			public Exception Exception
			{
				get
				{
					return asyncEx;
				}
			}

			public int TopicCount
			{
				get
				{
					return topicCount;
				}
			}
			#endregion

			#region Private / Protected
			private readonly BuildContext context;
			private readonly string configFile, manifestFile;
			private volatile bool assemblerError, canceled;
			private volatile int topicCount;
			private volatile Exception asyncEx;
			private Thread assemblerThread;
			private readonly BuildAssemblerOptions options;
			#endregion

			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="BuildAssemblerController" /> class.
			/// </summary>
			public BuildAssemblerController(BuildContext context, string configFile, string manifestFile, BuildAssemblerOptions options)
			{
				if (context == null)
					throw new ArgumentNullException("context");

				if (configFile == null)
					throw new ArgumentNullException("configFile");

				if (manifestFile == null)
					throw new ArgumentNullException("manifestFile");

				this.configFile = configFile;
				this.manifestFile = manifestFile;
				this.context = context;
				this.options = options;
			}
			#endregion

			#region Methods
			[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.LinkDemand,
			Flags = System.Security.Permissions.SecurityPermissionFlag.Infrastructure)]
			public override object InitializeLifetimeService()
			{
				return null;
			}

			public void Abort()
			{
				canceled = true;

				if (assemblerThread != null)
					assemblerThread.Abort();
			}

			public void Execute(string sandcastleToolsPath)
			{
				if (!BuildAssemblerLoader.TryLoadAssembly(sandcastleToolsPath))
					// This property will attempt to load the Build Assembler library for the current AppDomain.
					// It should never fail because the default AppDomain ensures that it can be loaded before
					// the current domain is even created.
					throw new InvalidOperationException(Errors.BuildAssemblerHarnessAPINotLoaded);

				using (EventWaitHandle waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset))
				{
					assemblerThread = new Thread(ExecuteAssembler);
					assemblerThread.Name = "BuildAssembler Thread";
					assemblerThread.IsBackground = true;

					if (!canceled)
					{
						assemblerThread.Start(waitHandle);

						waitHandle.WaitOne();
					}
				}
			}

			[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
			private void ExecuteAssembler(object state)
			{
				EventWaitHandle waitHandle = (EventWaitHandle) state;

				try
				{
					MessageHandler messageHandler = GetMessageHandler();
					BuildAssembler assembler = (messageHandler == null) ? new BuildAssembler() : new BuildAssembler(messageHandler);

					try
					{
						// This method call may take a lot of time before returning to the caller, so it's included in the async operation.
						// In testing it appears that the assembler immediately initializes each component from the specified configuration file
						// when the BuildAssembler.AddComponents(XPathNavigator) method is called.
						InitializeAssembler(assembler);

						topicCount = assembler.Apply(manifestFile);
					}
					finally
					{
						try
						{
							assembler.Dispose();
						}
						// In testing, disposing the build assembler may throw this exception
						catch (NullReferenceException) { }
					}
				}
				catch (ThreadAbortException)
				{
					// canceled; exit gracefully
					Thread.ResetAbort();
				}
				catch (Exception ex)
				{
					asyncEx = ex;
				}
				finally
				{
					waitHandle.Set();
				}
			}

			private void InitializeAssembler(BuildAssembler assembler)
			{
				XPathDocument config = new XPathDocument(configFile);
				XPathNavigator components = config.CreateNavigator().SelectSingleNode("/configuration/dduetools/builder/components");

				if (components != null)
					assembler.AddComponents(components);
			}

			#region Message Handlers
			private static string GetCategoryString(Type component)
			{
				if (component == null)
					return string.Empty;
				else
					return component.Name;
			}

			private MessageHandler GetMessageHandler()
			{
				StringBuilder method = new StringBuilder("Trace");
				bool traceEnabled = false;

				if ((options & BuildAssemblerOptions.TraceInformation) == BuildAssemblerOptions.TraceInformation)
				{
					method.Append("Info");
					traceEnabled = true;
				}

				if ((options & BuildAssemblerOptions.TraceWarnings) == BuildAssemblerOptions.TraceWarnings)
				{
					method.Append("Warn");
					traceEnabled = true;
				}

				if ((options & BuildAssemblerOptions.TraceErrors) == BuildAssemblerOptions.TraceErrors)
				{
					method.Append("Error");
					traceEnabled = true;
				}

				if (!traceEnabled)
					return null;
				else
					return (MessageHandler) Delegate.CreateDelegate(typeof(MessageHandler), this, method.ToString(), false, true);
			}

			private void TraceInfoWarnError(Type component, MessageLevel level, string message)
			{
				switch (level)
				{
					case MessageLevel.Info:
						context.TraceLine("{0}: {1}", level, message);
						break;
					case MessageLevel.Warn:
						context.AddWarningToList(GetCategoryString(component), message, level.ToString() + ": " + message + Environment.NewLine);
						break;
					case MessageLevel.Error:
						assemblerError = true;
						context.AddErrorToList(GetCategoryString(component), message, level.ToString() + ": " + message + Environment.NewLine);
						break;
				}
			}

			private void TraceInfoError(Type component, MessageLevel level, string message)
			{
				switch (level)
				{
					case MessageLevel.Info:
						context.TraceLine("{0}: {1}", level, message);
						break;
					case MessageLevel.Error:
						assemblerError = true;
						context.AddErrorToList(GetCategoryString(component), message, level.ToString() + ": " + message + Environment.NewLine);
						break;
				}
			}

			private void TraceInfoWarn(Type component, MessageLevel level, string message)
			{
				switch (level)
				{
					case MessageLevel.Info:
						context.TraceLine("{0}: {1}", level, message);
						break;
					case MessageLevel.Warn:
						context.AddWarningToList(GetCategoryString(component), message, level.ToString() + ": " + message + Environment.NewLine);
						break;
				}
			}

			private void TraceWarnError(Type component, MessageLevel level, string message)
			{
				switch (level)
				{
					case MessageLevel.Warn:
						context.AddWarningToList(GetCategoryString(component), message, level.ToString() + ": " + message + Environment.NewLine);
						break;
					case MessageLevel.Error:
						assemblerError = true;
						context.AddErrorToList(GetCategoryString(component), message, level.ToString() + ": " + message + Environment.NewLine);
						break;
				}
			}

			[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "component")]
			private void TraceInfo(Type component, MessageLevel level, string message)
			{
				switch (level)
				{
					case MessageLevel.Info:
						context.TraceLine("{0}: {1}", level, message);
						break;
				}
			}

			private void TraceWarn(Type component, MessageLevel level, string message)
			{
				switch (level)
				{
					case MessageLevel.Warn:
						context.AddWarningToList(GetCategoryString(component), message, level.ToString() + ": " + message + Environment.NewLine);
						break;
				}
			}

			private void TraceError(Type component, MessageLevel level, string message)
			{
				switch (level)
				{
					case MessageLevel.Error:
						assemblerError = true;
						context.AddErrorToList(GetCategoryString(component), message, level.ToString() + ": " + message + Environment.NewLine);
						break;
				}
			}
			#endregion
			#endregion
		}
		#endregion
	}
}
